refactor(crypto): 换用 CNG DPAPI#2962
Conversation
审阅者指南重构了密钥持久化逻辑,使用 PlainToolkit CNG DPAPI 和新的密钥格式版本,同时保持对旧的 DPAPI 保护密钥的向后兼容;将加密熵密钥常量集中管理,并新增基于 span 的 UTF-8 编码辅助方法,以便在与加密相关的代码中复用。 使用 CNG DPAPI 的更新版加密密钥持久化时序图sequenceDiagram
participant EncryptHelper
participant File as FileSystem
participant EncryptionData
participant ProtectedData
participant CngProtectedData
EncryptHelper->>File: File.Exists(keyFile)
alt keyFile exists
EncryptHelper->>File: File.ReadAllBytes(keyFile)
EncryptHelper->>EncryptionData: FromBytes(buf)
EncryptionData-->>EncryptHelper: EncryptionData data
alt data.Version == 1
EncryptHelper->>ProtectedData: Unprotect(data.Data, Key, DataProtectionScope.CurrentUser)
ProtectedData-->>EncryptHelper: encryptionKey
else data.Version == 2
EncryptHelper->>CngProtectedData: Unprotect(data.Data, Key, CngDataProtectionScope.CurrentUser)
CngProtectedData-->>EncryptHelper: encryptionKey
end
else keyFile missing
EncryptHelper->>EncryptHelper: RandomNumberGenerator.Fill(randomKey)
EncryptHelper->>CngProtectedData: Protect(randomKey, Key, CngDataProtectionScope.CurrentUser)
CngProtectedData-->>EncryptHelper: protectedData
EncryptHelper->>EncryptionData: ToBytes(new EncryptionData{ Version = 2, Data = protectedData })
EncryptionData-->>EncryptHelper: storeData
EncryptHelper->>File: new FileStream(tmpFile, Create, ReadWrite, None)
EncryptHelper->>File: FileStream.Write(storeData)
EncryptHelper->>File: FileStream.Flush(true)
EncryptHelper->>File: File.Move(tmpFile, keyFile, true)
EncryptHelper-->>EncryptHelper: encryptionKey = randomKey
end
文件级变更
针对关联 issue 的评估
技巧与命令与 Sourcery 交互
自定义你的体验访问你的 控制面板 来:
获取帮助Original review guide in EnglishReviewer's GuideRefactors the secret key persistence to use PlainToolkit CNG DPAPI with a new key format version while keeping backward compatibility with the old DPAPI-protected keys, centralizes the encryption entropy key constant, and adds span-based UTF-8 encoding helpers for reuse in crypto-related code. Sequence diagram for updated encryption key persistence with CNG DPAPIsequenceDiagram
participant EncryptHelper
participant File as FileSystem
participant EncryptionData
participant ProtectedData
participant CngProtectedData
EncryptHelper->>File: File.Exists(keyFile)
alt keyFile exists
EncryptHelper->>File: File.ReadAllBytes(keyFile)
EncryptHelper->>EncryptionData: FromBytes(buf)
EncryptionData-->>EncryptHelper: EncryptionData data
alt data.Version == 1
EncryptHelper->>ProtectedData: Unprotect(data.Data, Key, DataProtectionScope.CurrentUser)
ProtectedData-->>EncryptHelper: encryptionKey
else data.Version == 2
EncryptHelper->>CngProtectedData: Unprotect(data.Data, Key, CngDataProtectionScope.CurrentUser)
CngProtectedData-->>EncryptHelper: encryptionKey
end
else keyFile missing
EncryptHelper->>EncryptHelper: RandomNumberGenerator.Fill(randomKey)
EncryptHelper->>CngProtectedData: Protect(randomKey, Key, CngDataProtectionScope.CurrentUser)
CngProtectedData-->>EncryptHelper: protectedData
EncryptHelper->>EncryptionData: ToBytes(new EncryptionData{ Version = 2, Data = protectedData })
EncryptionData-->>EncryptHelper: storeData
EncryptHelper->>File: new FileStream(tmpFile, Create, ReadWrite, None)
EncryptHelper->>File: FileStream.Write(storeData)
EncryptHelper->>File: FileStream.Flush(true)
EncryptHelper->>File: File.Move(tmpFile, keyFile, true)
EncryptHelper-->>EncryptHelper: encryptionKey = randomKey
end
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - 我发现了两个问题,并给出了一些更高层次的反馈:
Key的熵 span 是用一个硬编码长度(stackalloc byte[22])和内联逻辑重新创建的;建议把这段逻辑抽取成一个小的辅助方法,通过Encoding.UTF8.GetByteCount(Key)计算出精确的字节数,以避免“魔法数字”和潜在的截断/填充问题。- 现在新生成的密钥会以版本 2 存储并使用 CNG DPAPI,而旧密钥仍然是版本 1,你可能需要考虑一个内联迁移路径(例如在加载时将 v1 密钥重新保护为 v2),以避免长期持久化混合密钥格式。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The entropy span for `Key` is recreated with a hard-coded length (`stackalloc byte[22]`) and inline logic; consider extracting this into a small helper that derives the exact byte count from `Encoding.UTF8.GetByteCount(Key)` to avoid magic numbers and potential truncation/padding issues.
- Now that new keys are stored as version 2 with CNG DPAPI while old ones remain at version 1, you may want to consider an inline migration path (e.g., re-protecting v1 keys as v2 upon load) to avoid persisting mixed key formats over time.
## Individual Comments
### Comment 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="156-158" />
<code_context>
{
var buf = File.ReadAllBytes(keyFile);
var data = EncryptionData.FromBytes(buf);
+ Span<byte> store = stackalloc byte[22];
+ Key.GetBytes(Encoding.UTF8, store);
+ var space = (ReadOnlySpan<byte>)store;
return data.Version switch
{
</code_context>
<issue_to_address>
**issue (bug_risk):** The stackalloc buffer length likely does not match the actual UTF-8 byte length of `Key`, which can break decryption for existing v1 keys.
Because the stackalloc span is 22 bytes, you’re passing 22 bytes (including an extra 0) as entropy to `ProtectedData.Unprotect`, while v1 keys were protected with a 21-byte `_IdentifyEntropy` buffer. DPAPI treats all entropy bytes as significant, so this off-by-one will prevent decryption of existing v1 keys. Please either use `Encoding.UTF8.GetBytes(Key)` as before, or size the span via `Encoding.UTF8.GetByteCount(Key)` and slice it to the actual number of bytes written from `Key.GetBytes`.
</issue_to_address>
### Comment 2
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="162" />
<code_context>
{
- 1 => ProtectedData.Unprotect(data.Data, _IdentifyEntropy, DataProtectionScope.CurrentUser),
+ 1 => ProtectedData.Unprotect(data.Data.AsSpan(), DataProtectionScope.CurrentUser, space),
+ 2 => CngProtectedData.Unprotect(data.Data.AsSpan(), CngDataProtectionScope.CurrentUser, space),
_ => throw new NotSupportedException("Unsupported key version")
};
</code_context>
<issue_to_address>
**issue (bug_risk):** Unprotect for v2 uses additional entropy, but Protect for v2 does not, which can cause decryption failures.
For v2, the key is protected without entropy but unprotected with `space` as entropy. If `CngProtectedData` matches DPAPI semantics, the entropy passed to `Unprotect` must be identical to what was used in `Protect`, so adding entropy only at unprotect time will cause decryption to fail. Align the calls by either omitting entropy on unprotect or using the same entropy during protect.
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的代码评审。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- The entropy span for
Keyis recreated with a hard-coded length (stackalloc byte[22]) and inline logic; consider extracting this into a small helper that derives the exact byte count fromEncoding.UTF8.GetByteCount(Key)to avoid magic numbers and potential truncation/padding issues. - Now that new keys are stored as version 2 with CNG DPAPI while old ones remain at version 1, you may want to consider an inline migration path (e.g., re-protecting v1 keys as v2 upon load) to avoid persisting mixed key formats over time.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The entropy span for `Key` is recreated with a hard-coded length (`stackalloc byte[22]`) and inline logic; consider extracting this into a small helper that derives the exact byte count from `Encoding.UTF8.GetByteCount(Key)` to avoid magic numbers and potential truncation/padding issues.
- Now that new keys are stored as version 2 with CNG DPAPI while old ones remain at version 1, you may want to consider an inline migration path (e.g., re-protecting v1 keys as v2 upon load) to avoid persisting mixed key formats over time.
## Individual Comments
### Comment 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="156-158" />
<code_context>
{
var buf = File.ReadAllBytes(keyFile);
var data = EncryptionData.FromBytes(buf);
+ Span<byte> store = stackalloc byte[22];
+ Key.GetBytes(Encoding.UTF8, store);
+ var space = (ReadOnlySpan<byte>)store;
return data.Version switch
{
</code_context>
<issue_to_address>
**issue (bug_risk):** The stackalloc buffer length likely does not match the actual UTF-8 byte length of `Key`, which can break decryption for existing v1 keys.
Because the stackalloc span is 22 bytes, you’re passing 22 bytes (including an extra 0) as entropy to `ProtectedData.Unprotect`, while v1 keys were protected with a 21-byte `_IdentifyEntropy` buffer. DPAPI treats all entropy bytes as significant, so this off-by-one will prevent decryption of existing v1 keys. Please either use `Encoding.UTF8.GetBytes(Key)` as before, or size the span via `Encoding.UTF8.GetByteCount(Key)` and slice it to the actual number of bytes written from `Key.GetBytes`.
</issue_to_address>
### Comment 2
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="162" />
<code_context>
{
- 1 => ProtectedData.Unprotect(data.Data, _IdentifyEntropy, DataProtectionScope.CurrentUser),
+ 1 => ProtectedData.Unprotect(data.Data.AsSpan(), DataProtectionScope.CurrentUser, space),
+ 2 => CngProtectedData.Unprotect(data.Data.AsSpan(), CngDataProtectionScope.CurrentUser, space),
_ => throw new NotSupportedException("Unsupported key version")
};
</code_context>
<issue_to_address>
**issue (bug_risk):** Unprotect for v2 uses additional entropy, but Protect for v2 does not, which can cause decryption failures.
For v2, the key is protected without entropy but unprotected with `space` as entropy. If `CngProtectedData` matches DPAPI semantics, the entropy passed to `Unprotect` must be identical to what was used in `Protect`, so adding entropy only at unprotect time will cause decryption to fail. Align the calls by either omitting entropy on unprotect or using the same entropy during protect.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了两个问题,并给出了一些整体层面的反馈:
- 新增的
ReadOnlySpan<char>.GetBytes扩展方法当前会调用ToString(),这违背了使用 span 的初衷并引入了额外的分配;建议直接使用Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>)这些重载,以避免中间字符串,并让这个工具方法真正保持对 span 友好。 - 鉴于
ReadOnlySpan<char>.GetBytes扩展目前只在固定的Key常量上使用,你可以考虑直接内联Encoding.UTF8.GetBytes(Key)(或使用一个静态缓存的byte[]),这样可以减少间接调用,同时避免引入一个可能在其他地方被误用或误解的通用扩展方法。
给 AI 代理的提示
Please address the comments from this code review:
## Overall Comments
- The new ReadOnlySpan<char>.GetBytes extension methods currently call ToString(), which defeats the purpose of using spans and adds an allocation; consider using the Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>) overloads directly to avoid intermediate strings and keep this utility truly span-friendly.
- Given the ReadOnlySpan<char>.GetBytes extension is only used for the fixed Key constant, you might simplify by inlining Encoding.UTF8.GetBytes(Key) (or a static cached byte[]), reducing indirection and avoiding introducing a general-purpose extension that may be misused or misunderstood elsewhere.
## Individual Comments
### Comment 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="159" />
<code_context>
return data.Version switch
{
- 1 => ProtectedData.Unprotect(data.Data, _IdentifyEntropy, DataProtectionScope.CurrentUser),
+ 1 => ProtectedData.Unprotect(data.Data, DataProtectionScope.CurrentUser, space),
+ 2 => CngProtectedData.Unprotect(data.Data, CngDataProtectionScope.CurrentUser, space),
_ => throw new NotSupportedException("Unsupported key version")
</code_context>
<issue_to_address>
**issue (bug_risk):** ProtectedData.Unprotect arguments appear to be in the wrong order compared to the standard API.
The original call `ProtectedData.Unprotect(data.Data, _IdentifyEntropy, DataProtectionScope.CurrentUser)` matches the API `(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope)`. The new call `ProtectedData.Unprotect(data.Data, DataProtectionScope.CurrentUser, space)` swaps `optionalEntropy` and `scope`, which will either fail to compile or change behavior. This should be `ProtectedData.Unprotect(data.Data, space, DataProtectionScope.CurrentUser)` instead.
</issue_to_address>
### Comment 2
<location path="PCL.Core/Utils/Exts/StringExtension.cs" line_range="296-300" />
<code_context>
+ return encode.GetBytes(str.ToString());
+ }
+
+ public void GetBytes(Encoding? encode, Span<byte> space)
+ {
+ encode ??= Encoding.UTF8;
+ encode.GetBytes(str, space);
+ }
+ }
</code_context>
<issue_to_address>
**suggestion:** Consider returning the number of bytes written from the span-based GetBytes overload.
This overload currently returns `void`, whereas `Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>)` returns an `int` with the number of bytes written. Returning that `int` here (and optionally renaming `space` to something like `destination`) would give callers a reliable way to know how many bytes in the span are valid.
```suggestion
public int GetBytes(Encoding? encode = null, Span<byte> destination)
{
encode ??= Encoding.UTF8;
return encode.GetBytes(str, destination);
}
```
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- The new ReadOnlySpan.GetBytes extension methods currently call ToString(), which defeats the purpose of using spans and adds an allocation; consider using the Encoding.GetBytes(ReadOnlySpan, Span) overloads directly to avoid intermediate strings and keep this utility truly span-friendly.
- Given the ReadOnlySpan.GetBytes extension is only used for the fixed Key constant, you might simplify by inlining Encoding.UTF8.GetBytes(Key) (or a static cached byte[]), reducing indirection and avoiding introducing a general-purpose extension that may be misused or misunderstood elsewhere.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new ReadOnlySpan<char>.GetBytes extension methods currently call ToString(), which defeats the purpose of using spans and adds an allocation; consider using the Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>) overloads directly to avoid intermediate strings and keep this utility truly span-friendly.
- Given the ReadOnlySpan<char>.GetBytes extension is only used for the fixed Key constant, you might simplify by inlining Encoding.UTF8.GetBytes(Key) (or a static cached byte[]), reducing indirection and avoiding introducing a general-purpose extension that may be misused or misunderstood elsewhere.
## Individual Comments
### Comment 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="159" />
<code_context>
return data.Version switch
{
- 1 => ProtectedData.Unprotect(data.Data, _IdentifyEntropy, DataProtectionScope.CurrentUser),
+ 1 => ProtectedData.Unprotect(data.Data, DataProtectionScope.CurrentUser, space),
+ 2 => CngProtectedData.Unprotect(data.Data, CngDataProtectionScope.CurrentUser, space),
_ => throw new NotSupportedException("Unsupported key version")
</code_context>
<issue_to_address>
**issue (bug_risk):** ProtectedData.Unprotect arguments appear to be in the wrong order compared to the standard API.
The original call `ProtectedData.Unprotect(data.Data, _IdentifyEntropy, DataProtectionScope.CurrentUser)` matches the API `(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope)`. The new call `ProtectedData.Unprotect(data.Data, DataProtectionScope.CurrentUser, space)` swaps `optionalEntropy` and `scope`, which will either fail to compile or change behavior. This should be `ProtectedData.Unprotect(data.Data, space, DataProtectionScope.CurrentUser)` instead.
</issue_to_address>
### Comment 2
<location path="PCL.Core/Utils/Exts/StringExtension.cs" line_range="296-300" />
<code_context>
+ return encode.GetBytes(str.ToString());
+ }
+
+ public void GetBytes(Encoding? encode, Span<byte> space)
+ {
+ encode ??= Encoding.UTF8;
+ encode.GetBytes(str, space);
+ }
+ }
</code_context>
<issue_to_address>
**suggestion:** Consider returning the number of bytes written from the span-based GetBytes overload.
This overload currently returns `void`, whereas `Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>)` returns an `int` with the number of bytes written. Returning that `int` here (and optionally renaming `space` to something like `destination`) would give callers a reliable way to know how many bytes in the span are valid.
```suggestion
public int GetBytes(Encoding? encode = null, Span<byte> destination)
{
encode ??= Encoding.UTF8;
return encode.GetBytes(str, destination);
}
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Changed Key to a readonly byte array and updated usage in _GetKey method.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了两个问题,并给出了一些整体性反馈:
- 新增的
Key字段被声明为readnoly,并使用"PCL CE Encryption Key".GetBytes();按当前写法代码无法通过编译(readonly拼写错误,而且GetBytes是定义在ReadOnlySpan<char>上的扩展方法),所以要么保留原来的Encoding.UTF8.GetBytes,要么为string添加一个合适的扩展重载。 - 在
_GetKey中,调用CngProtectedData.Protect(randomKey, CngDataProtectionScope.CurrentUser, space)时引用了未定义的变量space;如果你是想传入熵(entropy)密钥,这里很可能应该是Key(或者另一个显式参数)。 - 新增的
ReadOnlySpan<char>扩展方法GetBytes(Encoding? encode = null)调用了str.ToString()再去编码,这样会削弱使用 span 的意义;建议使用基于 span 的Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>)重载(或类似方式),以避免中间字符串分配。
面向 AI 代理的提示词
Please address the comments from this code review:
## Overall Comments
- The new `Key` field is declared as `readnoly` and uses `"PCL CE Encryption Key".GetBytes()`; this won’t compile as written (typo on `readonly` and `GetBytes` is defined as a `ReadOnlySpan<char>` extension), so either keep the original `Encoding.UTF8.GetBytes` or add a proper `string` extension overload.
- In `_GetKey`, the call `CngProtectedData.Protect(randomKey, CngDataProtectionScope.CurrentUser, space)` references an undefined `space` variable; if you intended to pass the entropy key, this should likely be `Key` (or another explicit parameter) instead.
- The new `ReadOnlySpan<char>` extension `GetBytes(Encoding? encode = null)` calls `str.ToString()` and then encodes it, which defeats the purpose of using spans; consider using the span-based `Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>)` overload (or similar) to avoid the intermediate string allocation.
## Individual Comments
### Comment 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="16-19" />
<code_context>
public static class EncryptHelper
{
+ private static readnoly byte[] Key = "PCL CE Encryption Key".GetBytes();
public static (IEncryptionProvider Provider, uint Version) DefaultProvider => _DefaultProvider.Value;
private static readonly Lazy<(IEncryptionProvider Provider, uint Version)> _DefaultProvider = new(_SelectBestEncryption);
</code_context>
<issue_to_address>
**issue (typo):** Typo in `readnoly` will prevent this field from compiling.
Use the `readonly` keyword here; as written, this line won’t compile and the `Key` field won’t be defined correctly.
```suggestion
public static class EncryptHelper
{
private static readonly byte[] Key = "PCL CE Encryption Key".GetBytes();
public static (IEncryptionProvider Provider, uint Version) DefaultProvider => _DefaultProvider.Value;
```
</issue_to_address>
### Comment 2
<location path="PCL.Core/Utils/Exts/StringExtension.cs" line_range="290-293" />
<code_context>
+
+ extension(ReadOnlySpan<char> str)
+ {
+ public byte[] GetBytes(Encoding? encode = null)
+ {
+ encode ??= Encoding.UTF8;
+ return encode.GetBytes(str.ToString());
+ }
+
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid allocating an intermediate string when encoding a `ReadOnlySpan<char>`.
`str.ToString()` adds an unnecessary allocation. Where supported, prefer the `Encoding.GetBytes(ReadOnlySpan<char>)` overload for spans. If you need to support older frameworks, consider `GetByteCount` + a rented/pooled buffer to avoid extra allocations in hot paths.
Suggested implementation:
```csharp
public byte[] GetBytes(Encoding? encode = null)
{
encode ??= Encoding.UTF8;
return encode.GetBytes(str);
}
```
If this project must target frameworks that do not support `Encoding.GetBytes(ReadOnlySpan<char>)` (e.g., .NET Framework or .NET Standard 2.0), you will need to replace the direct `encode.GetBytes(str)` call with a fallback that:
1. Uses `encode.GetByteCount(str)` to determine the byte count.
2. Allocates or rents a `byte[]` of the required size (e.g., via `ArrayPool<byte>.Shared.Rent` in hot paths).
3. Calls `encode.GetBytes(str, destinationSpan)` to fill the buffer.
4. Returns either the exact-sized array or the rented buffer (trimming/copying if necessary).
You can use `#if` conditionals around the span-based overload to keep compatibility with older targets if needed.
</issue_to_address>帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- The new
Keyfield is declared asreadnolyand uses"PCL CE Encryption Key".GetBytes(); this won’t compile as written (typo onreadonlyandGetBytesis defined as aReadOnlySpan<char>extension), so either keep the originalEncoding.UTF8.GetBytesor add a properstringextension overload. - In
_GetKey, the callCngProtectedData.Protect(randomKey, CngDataProtectionScope.CurrentUser, space)references an undefinedspacevariable; if you intended to pass the entropy key, this should likely beKey(or another explicit parameter) instead. - The new
ReadOnlySpan<char>extensionGetBytes(Encoding? encode = null)callsstr.ToString()and then encodes it, which defeats the purpose of using spans; consider using the span-basedEncoding.GetBytes(ReadOnlySpan<char>, Span<byte>)overload (or similar) to avoid the intermediate string allocation.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new `Key` field is declared as `readnoly` and uses `"PCL CE Encryption Key".GetBytes()`; this won’t compile as written (typo on `readonly` and `GetBytes` is defined as a `ReadOnlySpan<char>` extension), so either keep the original `Encoding.UTF8.GetBytes` or add a proper `string` extension overload.
- In `_GetKey`, the call `CngProtectedData.Protect(randomKey, CngDataProtectionScope.CurrentUser, space)` references an undefined `space` variable; if you intended to pass the entropy key, this should likely be `Key` (or another explicit parameter) instead.
- The new `ReadOnlySpan<char>` extension `GetBytes(Encoding? encode = null)` calls `str.ToString()` and then encodes it, which defeats the purpose of using spans; consider using the span-based `Encoding.GetBytes(ReadOnlySpan<char>, Span<byte>)` overload (or similar) to avoid the intermediate string allocation.
## Individual Comments
### Comment 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="16-19" />
<code_context>
public static class EncryptHelper
{
+ private static readnoly byte[] Key = "PCL CE Encryption Key".GetBytes();
public static (IEncryptionProvider Provider, uint Version) DefaultProvider => _DefaultProvider.Value;
private static readonly Lazy<(IEncryptionProvider Provider, uint Version)> _DefaultProvider = new(_SelectBestEncryption);
</code_context>
<issue_to_address>
**issue (typo):** Typo in `readnoly` will prevent this field from compiling.
Use the `readonly` keyword here; as written, this line won’t compile and the `Key` field won’t be defined correctly.
```suggestion
public static class EncryptHelper
{
private static readonly byte[] Key = "PCL CE Encryption Key".GetBytes();
public static (IEncryptionProvider Provider, uint Version) DefaultProvider => _DefaultProvider.Value;
```
</issue_to_address>
### Comment 2
<location path="PCL.Core/Utils/Exts/StringExtension.cs" line_range="290-293" />
<code_context>
+
+ extension(ReadOnlySpan<char> str)
+ {
+ public byte[] GetBytes(Encoding? encode = null)
+ {
+ encode ??= Encoding.UTF8;
+ return encode.GetBytes(str.ToString());
+ }
+
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid allocating an intermediate string when encoding a `ReadOnlySpan<char>`.
`str.ToString()` adds an unnecessary allocation. Where supported, prefer the `Encoding.GetBytes(ReadOnlySpan<char>)` overload for spans. If you need to support older frameworks, consider `GetByteCount` + a rented/pooled buffer to avoid extra allocations in hot paths.
Suggested implementation:
```csharp
public byte[] GetBytes(Encoding? encode = null)
{
encode ??= Encoding.UTF8;
return encode.GetBytes(str);
}
```
If this project must target frameworks that do not support `Encoding.GetBytes(ReadOnlySpan<char>)` (e.g., .NET Framework or .NET Standard 2.0), you will need to replace the direct `encode.GetBytes(str)` call with a fallback that:
1. Uses `encode.GetByteCount(str)` to determine the byte count.
2. Allocates or rents a `byte[]` of the required size (e.g., via `ArrayPool<byte>.Shared.Rent` in hot paths).
3. Calls `encode.GetBytes(str, destinationSpan)` to fill the buffer.
4. Returns either the exact-sized array or the rented buffer (trimming/copying if necessary).
You can use `#if` conditionals around the span-based overload to keep compatibility with older targets if needed.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
珍爱生命,远离 GitHub WebEditor
Refactor StringExtension methods to improve performance and readability.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我在这里给出了一些整体性的反馈:
EncryptHelper中新的Key熵值目前以可变的byte[]形式保存;建议将其暴露为ReadOnlyMemory<byte>,或者通过属性返回一个新的 span,以避免对整个进程范围内的加密常量发生意外修改。- 新增的字符串/
ReadOnlySpan<char>GetBytes帮助方法,把加密代码依赖到了通用的字符串扩展上;如果这些方法主要是为加密场景准备的,建议考虑将它们移动到更底层或更偏向加密的工具类中,以保持更清晰的分层结构。 - 对于
ReadOnlySpan<char>.GetBytes(Span<byte> destination, Encoding? encode = null)这个扩展方法,可能需要在文档里说明或封装一下GetByteCount/GetBytes的使用模式,这样调用方就能更容易地确定所需缓冲区大小,避免在目标 span 过小时出现意料之外的ArgumentException。
给 AI Agent 的提示
Please address the comments from this code review:
## Overall Comments
- The new `Key` entropy value in `EncryptHelper` is stored as a mutable `byte[]`; consider exposing it as a `ReadOnlyMemory<byte>` or returning a new span from a property to avoid accidental mutation of a process-wide crypto constant.
- The new string/`ReadOnlySpan<char>` `GetBytes` helpers introduce a dependency from crypto code to the general string extensions; if these are meant primarily for crypto usage, consider moving them to a more low-level/crypto-focused helper to keep layering clearer.
- For the `ReadOnlySpan<char>.GetBytes(Span<byte> destination, Encoding? encode = null)` extension, it might be helpful to document or wrap the `GetByteCount`/`GetBytes` pattern so callers can easily determine the required buffer size and avoid unexpected `ArgumentException` when the destination span is too small.帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English
Hey - I've left some high level feedback:
- The new
Keyentropy value inEncryptHelperis stored as a mutablebyte[]; consider exposing it as aReadOnlyMemory<byte>or returning a new span from a property to avoid accidental mutation of a process-wide crypto constant. - The new string/
ReadOnlySpan<char>GetByteshelpers introduce a dependency from crypto code to the general string extensions; if these are meant primarily for crypto usage, consider moving them to a more low-level/crypto-focused helper to keep layering clearer. - For the
ReadOnlySpan<char>.GetBytes(Span<byte> destination, Encoding? encode = null)extension, it might be helpful to document or wrap theGetByteCount/GetBytespattern so callers can easily determine the required buffer size and avoid unexpectedArgumentExceptionwhen the destination span is too small.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new `Key` entropy value in `EncryptHelper` is stored as a mutable `byte[]`; consider exposing it as a `ReadOnlyMemory<byte>` or returning a new span from a property to avoid accidental mutation of a process-wide crypto constant.
- The new string/`ReadOnlySpan<char>` `GetBytes` helpers introduce a dependency from crypto code to the general string extensions; if these are meant primarily for crypto usage, consider moving them to a more low-level/crypto-focused helper to keep layering clearer.
- For the `ReadOnlySpan<char>.GetBytes(Span<byte> destination, Encoding? encode = null)` extension, it might be helpful to document or wrap the `GetByteCount`/`GetBytes` pattern so callers can easily determine the required buffer size and avoid unexpected `ArgumentException` when the destination span is too small.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
将 DPAPI 改为 CNG DPAPI,并优化了代码结构
Resolved #2687
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements:
新功能:
增强改进:
ReadOnlySpan<char>扩展辅助方法,以获取可复用的 UTF-8 字节表示,用于加密相关代码中。Original summary in English
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements:
新特性:
增强内容:
ReadOnlySpan<char>值的 UTF-8 字节表示,以便在与加密相关的代码中复用。Original summary in English
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements:
新功能:
增强改进:
ReadOnlySpan<char>扩展辅助方法,以获取可复用的 UTF-8 字节表示,用于加密相关代码中。Original summary in English
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements:
增强内容:
Original summary in English
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements:
新功能:
增强改进:
ReadOnlySpan<char>扩展辅助方法,以获取可复用的 UTF-8 字节表示,用于加密相关代码中。Original summary in English
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements:
新特性:
增强内容:
ReadOnlySpan<char>值的 UTF-8 字节表示,以便在与加密相关的代码中复用。Original summary in English
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements:
新功能:
增强改进:
ReadOnlySpan<char>扩展辅助方法,以获取可复用的 UTF-8 字节表示,用于加密相关代码中。Original summary in English
Summary by Sourcery
将加密密钥持久化方式切换为使用基于 CNG 的 DPAPI,同时保持与现有由 DPAPI 保护的密钥的兼容性。
New Features:
Enhancements:
string和ReadOnlySpan<char>的扩展辅助方法,以获取 UTF-8 字节表示,用于在加密相关代码中复用。Original summary in English
Summary by Sourcery
Switch the encryption key persistence to use CNG-based DPAPI while preserving compatibility with existing DPAPI-protected keys.
New Features:
Enhancements: